﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Threading.Tasks;

namespace ChatServer
{
    class Server : Chat.IMessageDelegate
    {
        // Constants 
        const int kConnectionQueueSize = 100;
        const int kDefaultPort = 999;

        // Constructors

        public Server() : this(kDefaultPort) { }

        public Server(int listeningPort)
        {
            System.Diagnostics.Debug.Assert(listeningPort > 0 && listeningPort < 65535);
            m_chatPort = listeningPort;
        }

        // Methods 

        public void Start()
        {

            List<IPAddress> AddressesList = new List<IPAddress>();
            NetworkInterface[] adapters = NetworkInterface.GetAllNetworkInterfaces();

            foreach (NetworkInterface adapter in adapters)
            {
                IPInterfaceProperties properties = adapter.GetIPProperties();
                foreach (IPAddressInformation uniCast in properties.UnicastAddresses)
                {
                    if (!IPAddress.IsLoopback(uniCast.Address) && uniCast.Address.AddressFamily != AddressFamily.InterNetworkV6)
                    {
                        AddressesList.Add(uniCast.Address);
                    }
                }
            }

            foreach(IPAddress address in AddressesList)
            {
                Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
                IPEndPoint endPoint = new IPEndPoint(address, m_chatPort);

                try
                {
                    Chat.Logger.Debug(String.Format("Found address {0}", address.ToString()));

                    socket.Bind(endPoint);

                    Task.Factory.StartNew(() => StartListenSocket(socket));
                }
                catch (SocketException e)
                {
                    Chat.Logger.Error(String.Format("Failed to bind to address {0} due to reason: {1}", address.ToString(), e.Message));
                }
            }
        }

        public void Stop() {}

        private void StartListenSocket(Socket socket)
        {
            socket.Blocking = true;

            socket.Listen(kConnectionQueueSize);

            while (true)
            {
                Socket incomingSocket = null;
                byte[] userIdBytes = new byte[Chat.ChatData.MaxLengthForKey(Chat.ChatData.Key.SenderId) + 1];

                try
                {
                    incomingSocket = socket.Accept();

                    Task.Factory.StartNew(() =>
                        {
                            String userId = Chat.DataPacket.Receive(incomingSocket);

                            if (Chat.ChatData.IsValidData(Chat.ChatData.Key.SenderId, userId))
                            {
                                Chat.Connection connection = ConnectionByUsername(userId);
                                if (connection == null)
                                {
                                    Chat.Connection newConnection = new Chat.Connection(incomingSocket, userId, this);
                                    
                                    lock (m_connectionsMap)
                                    {
                                        m_connectionsMap.Add(userId, newConnection);
                                    }

                                    Chat.Logger.Info(String.Format("user {0} joined chat from address {1}", userId, newConnection.GetRemoteIp()));
                                    Chat.DataPacket.Send(incomingSocket, "OK");
                                }
                                else
                                {
                                    Chat.Logger.Error(String.Format("user {0} already in chat from address {1}", userId, connection.GetRemoteIp()));
                                    Chat.DataPacket.Send(incomingSocket, "Exists");
                                }
                            }
                            else
                            {
                                Chat.Logger.Error(String.Format("got invalid username {0} from address {1}", userId, incomingSocket.RemoteEndPoint.ToString()));
                                incomingSocket.Disconnect(false);
                            }
                        }
                    );
                }
                catch (SocketException e)
                {
                    Chat.Logger.Error(String.Format("Got socket exception: {0}", e.Message));
                }

            }
        }

        private Chat.Connection ConnectionByUsername(String username)
        {
            lock (m_connectionsMap)
            {
                if (m_connectionsMap.Count == 0)
                {
                    return null;
                }

                Chat.Connection result = null;

                if (!m_connectionsMap.TryGetValue(username, out result))
                {
                    return null;
                }

                return result;
            }
        }

        private Chat.ChatRoom RoomByRoomName(String room)
        {
            lock (m_roomsMap)
            {
                if (m_roomsMap.Count == 0)
                {
                    return null;
                }

                Chat.ChatRoom result = null;

                if (!m_roomsMap.TryGetValue(room, out result))
                {
                    return null;
                }

                return result;
            }
        }

        bool AddUserToRoom(String user, String roomName)
        {
            if (!roomName.Equals(String.Empty) && Chat.ChatData.IsValidData(Chat.ChatData.Key.RoomId, roomName))
            {
                Chat.ChatRoom room = RoomByRoomName(roomName);

                if (room == null)
                {
                    room = new Chat.ChatRoom(roomName, this);
                    m_roomsMap.Add(roomName, room);
                }

                Chat.Logger.Info(String.Format("user {0} joined to room {1}", user, roomName));
                room.AddUser(user);
                return true;
            }

            return false;
        }

        bool RemoveUserFromRoom(String user, String roomName)
        {
            if (!roomName.Equals(String.Empty) && Chat.ChatData.IsValidData(Chat.ChatData.Key.RoomId, roomName))
            {
                Chat.ChatRoom room = RoomByRoomName(roomName);

                if (room == null)
                {
                    return false;
                }

                Chat.Logger.Info(String.Format("user {0} joined to room {1}", user, roomName));
                room.RemoveUser(user);
                return true;
            }

            return false;
        }

        // Chat.IMessageDelegate interface implementation

        public bool GotMessage(String senderId, String receiverId, String message)
        {
            Chat.ChatCommand.Command command = Chat.ChatCommand.GetCommand(message);

            if (command == Chat.ChatCommand.Command.JoinRoom)
            {
                String roomName = Chat.ChatCommand.GetCommandArgument(message);

                return AddUserToRoom(senderId, roomName);
            }

            if (command == Chat.ChatCommand.Command.LeaveRoom)
            {
                String roomName = Chat.ChatCommand.GetCommandArgument(message);

                return RemoveUserFromRoom(senderId, roomName);
            }

            Chat.Connection receiver = ConnectionByUsername(receiverId);

            if (receiver != null && receiver.GetUserId().Equals(receiverId))
            {
                receiver.SendMessage(senderId, receiverId, message);
                return true;
            }

            Chat.ChatRoom receiverRoom = RoomByRoomName(receiverId);

            if (receiverRoom != null && receiverRoom.GetRoomName().Equals(receiverId))
            {
                receiverRoom.SendMessage(senderId, message);
                return true;
            }

            return false;
        }

        public void GotDisconnectRequest(Chat.Connection connection)
        {
            Chat.Logger.Info(String.Format("user {0} disconnected from address {1}", connection.GetUserId(), connection.GetRemoteIp()));

            lock (m_connectionsMap)
            {
                m_connectionsMap.Remove(connection.GetUserId());
            }
        }

        public void GotDeliveryStatus(Chat.Connection connection, Chat.ChatData.DeliveryStatus status)
        {
            if (status == Chat.ChatData.DeliveryStatus.OK)
            {
                Chat.Logger.Info(String.Format("Delivery status OK from user {0}, address {1}", connection.GetUserId(), connection.GetRemoteIp()));
            }
            else if (status == Chat.ChatData.DeliveryStatus.UserNotFound)
            {
                Chat.Logger.Info(String.Format("Delivery status UserNotFound from user {0}, address {1}", connection.GetUserId(), connection.GetRemoteIp()));
            }
            else
            {
                Chat.Logger.Info(String.Format("Invalid delivery status from user {0}, address {1}", connection.GetUserId(), connection.GetRemoteIp()));
            }
        }

        // Private data

        private Dictionary<String, Chat.Connection> m_connectionsMap = new Dictionary<String, Chat.Connection>();
        private Dictionary<String, Chat.ChatRoom> m_roomsMap = new Dictionary<String, Chat.ChatRoom>();
        int m_chatPort;
    }
}
